﻿using System;
using System.Collections;
using System.Linq;
using System.Text;
using System.IO;

namespace NetDns
{
    /// <summary>
    /// ByteStream is the replacement of the 'Pointer' class which is 'readonly'.
    /// Bytes stream reads and writes in network byte order (big endian). x86 hardware is by default little endian.
    /// This stream is basicly a wrapper with c# versions of the common 'C' htons, htonl, ntohs and ntohl macros (host-to-network)
    /// </summary>
    class ByteStream : Stream
    {
        protected ByteStreamAccess access = ByteStreamAccess.Read;
        protected byte[] stream;
        protected long position;
        protected long maxLength = long.MaxValue;

        public ByteStream(byte[] data)
        {
            this.stream = data;
            this.position = 0;
        }
        public ByteStream(byte[] data, long position)
        {
            this.stream = data;
            this.position = position;
        }
        public ByteStream(byte[] data, long position, ByteStreamAccess access)
        {
            this.stream = data;
            this.position = position;
            this.access = access;
        }

        /// <summary>
        /// Position of the stream pointer
        /// </summary>
        public override long Position
        {
            get { return this.position; }
            set
            {
                if ( value < 0 )
                    throw new ArgumentOutOfRangeException( "position", "Position can't be negative" );
                if ( value > this.stream.Length )
                    throw new ArgumentOutOfRangeException( "position" );

                this.position = value;
            }
        }

        /// <summary>
        /// Returns a (deep) copy of the ByteStream instance
        /// </summary>
        /// <returns>A new ByteStream instance</returns>
        public ByteStream Copy()
        {
            return new ByteStream( this.stream, this.position, this.access );
        }

        /// <summary>
        /// Return the next byte in the stream with changing the position pointer
        /// </summary>
        /// <returns>next byte in stream</returns>
        public byte Peek()
        {
            if ( !CanRead )
                throw new IOException( "The stream is opened in 'write' access. Reading is not possible" );

            return this.stream[this.position];
        }

        /// <summary>
        /// Positions the pointer position in relation with 'origin'.
        /// </summary>
        /// <param name="offset"></param>
        /// <param name="origin"></param>
        /// <returns></returns>
        public override long Seek(long offset, SeekOrigin origin)
        {
            switch ( origin )
            {
                case SeekOrigin.Begin: this.position = offset; break;
                case SeekOrigin.Current: this.position += offset; break;
                case SeekOrigin.End: this.position = this.Length - offset; break;
            }
            return this.position;
        }

        /// <summary>
        /// Sets the maximum length of the stream
        /// </summary>
        /// <param name="value"></param>
        public override void SetLength(long value)
        {
            this.maxLength = value;
        }

        /// <summary>
        /// Length of the stream
        /// </summary>
        public override long Length { get { return this.stream.Length; } }
        public override int Read(byte[] buffer, int offset, int count)
        {
            if ( !CanRead )
                throw new IOException( "The stream is opened in 'write' access. Reading is not possible" );

            long length = count;
            if ( this.stream.Length - this.position < count )
                length = (long) this.stream.Length - this.position;

            Array.Copy( this.stream, this.position, buffer, offset, Length );
            this.position += length;
            return (int) length;
        }

        /// <summary>
        /// Reads a single byte from the stream
        /// </summary>
        /// <returns></returns>
        public byte ReadByte()
        {
            if ( !CanRead )
                throw new IOException( "The stream is opened in 'write' access. Reading is not possible" );

            return this.stream[this.position++];
        }

        /// <summary>
        /// Return heximal encoding of the byte
        /// </summary>
        /// <returns></returns>
        public string ReadBinaryCharacter()
        {
            return ReadByte().ToString( "X2" );
        }
        /// <summary>
        /// Reads a 16-bit integer from the stream
        /// </summary>
        /// <returns>A 16-bit integer (short)</returns>
        public short ReadShort()
        {
            byte[] data = new byte[2];
            this.Read( data, 0, 2 );

            if ( BitConverter.IsLittleEndian )
                Array.Reverse( data );    //reverse byte order

            return BitConverter.ToInt16( data, 2 );
        }

        /// <summary>
        /// Reads a 32-bit integer from the stream
        /// </summary>
        /// <returns>A 32-bit integer (int)</returns>
        public int ReadInt()
        {
            byte[] data = new byte[4];
            this.Read( data, 0, 4 );

            if ( BitConverter.IsLittleEndian )
                Array.Reverse( data );    //reverse byte order

            return BitConverter.ToInt32( data, 2 );

        }

        /// <summary>
        /// Reads a 64-bit integer from the stream
        /// </summary>
        /// <returns>A 64-bit integer (long)</returns>
        public long ReadLong()
        {
            byte[] data = new byte[8];
            this.Read( data, 0, 8 );

            if ( BitConverter.IsLittleEndian )
                Array.Reverse( data );    //reverse byte order

            return BitConverter.ToInt64( data, 2 );
        }




        /// <summary>
        ///
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="offset"></param>
        /// <param name="count"></param>
        public override void Write(byte[] buffer, int offset, int count)
        {
            if ( !CanWrite )
                throw new IOException( "The stream is opened in 'read' access. Writing is not possible" );


            long length = count;
            if ( length > ( buffer.Length - offset ) )
                length = buffer.Length - offset;

            //Read the 'buffer' till the stream maximum is reached
            if ( length > this.maxLength )
                length = this.maxLength - this.stream.Length;

            if ( length == 0 )
                throw new IOException( "Maximum size of stream is reached!" );

            byte[] data = new byte[this.stream.Length + length];

            //Merge current stream with 'buffer' array
            Array.Copy( this.stream, 0, data, 0, this.stream.Length );
            Array.Copy( buffer, offset, data, this.stream.Length, length );

            //assign new byte array and reset position
            this.stream = data;
            this.position = data.Length;

        }
        /// <summary>
        /// Writes a single (raw) byte to the stream
        /// </summary>
        /// <param name="value"></param>
        public override void WriteByte(byte value)
        {
            if ( !CanWrite )
                throw new IOException( "The stream is opened in 'read' access. Writing is not possible" );

            if ( this.stream.Length + 1 > this.maxLength )
                throw new IOException( "Maximum size of stream is reached!" );

            ArrayList s = new ArrayList( this.stream.Length + 1 );
            s.AddRange( this.stream );
            s.Add( value );
            byte[] data = new byte[s.Count];
            data.CopyTo( data, 0 );

            //assign new byte array and reset position
            this.stream = data;
            this.position = data.Length;

        }

        /// <summary>
        /// Writes a 16 bit integer in network byte order to the stream
        /// </summary>
        /// <param name="value">Value to write</param>
        public void WriteShort(short value)
        {
            byte[] v = BitConverter.GetBytes( value );
            if ( BitConverter.IsLittleEndian )
                Array.Reverse( v );   //reverse byte

            this.Write( v, 0, v.Length );
        }

        /// <summary>
        /// Writes an 32-bit integer in network byte order to the stream
        /// </summary>
        /// <param name="value">32-bit value to write</param>
        public void WriteInt(int value)
        {
            byte[] v = BitConverter.GetBytes( value );
            if ( BitConverter.IsLittleEndian )
                Array.Reverse( v );   //reverse byte

            this.Write( v, 0, v.Length );
        }

        /// <summary>
        /// Writes an 64-bit integer in network byte order (big endian) to the stream
        /// </summary>
        /// <param name="value"></param>
        public void WriteLong(long value)
        {
            byte[] v = BitConverter.GetBytes( value );
            if ( BitConverter.IsLittleEndian )
                Array.Reverse( v );   //reverse byte

            this.Write( v, 0, v.Length );
        }


        public override void Close() { }
        public override void Flush() { }

        public override bool CanRead { get { return this.access == ByteStreamAccess.Read; } }
        public override bool CanSeek { get { return true; } }
        public override bool CanWrite { get { return this.access == ByteStreamAccess.Write; } }
        public override bool CanTimeout { get { return false; } }

    }

    public enum ByteStreamAccess
    {
        Read,
        Write
    }


    public delegate void DataAvailableHandler();

    /// <summary>
    /// A ReadWriteStream supports writing and reading at the same time. This stream is platform independed Byte Order stream. 
    /// However by default we assume x86 hardware, which means reading in Host Byte Order (little endian) and writing in Network Byte Order (big endian)
    /// </summary>
    public class ReadWriteStream : Stream, ICloneable
    {
        private const int IncreaseSize = 1024;
        protected byte[] stream;
        protected long length;
        protected long readPosition;
        protected long writePosition;
        public event DataAvailableHandler DataAvailable;

        public ReadWriteStream()
        {
            this.stream = new byte[1024];
            this.readPosition = 0;
            this.writePosition = 0;
            this.length = 0;
        }

        public ReadWriteStream(byte[] data)
        {
            this.stream = data;
            this.readPosition = 0;
            this.writePosition = data.Length;
            this.length = data.Length;
        }
        public ReadWriteStream(byte[] data, long readPosition, long writePosition)
        {
            this.stream = data;
            this.readPosition = readPosition;
            this.writePosition = writePosition;
            this.length = data.Length;
        }
        public ReadWriteStream(byte[] data, long readPosition, long writePosition, long length)
        {
            this.stream = data;
            this.readPosition = readPosition;
            this.writePosition = writePosition;
            this.length = length;
        }

        /// <summary>
        /// An 'deep' copied ReadWriteStream instance including any 'DataAvailable' listners
        /// </summary>
        /// <returns></returns>
        public ReadWriteStream Copy()
        {
            ReadWriteStream rws = new ReadWriteStream( this.stream, this.readPosition, this.writePosition, this.length );
            rws.DataAvailable = this.DataAvailable; //attach DataAvailable listners

            return rws;
        }
        /// <summary>
        /// A 'deep' copied ReadWriteStream of this instance without any 'DataAvailable' listners.
        /// </summary>
        /// <returns></returns>
        public object Clone()
        {
            return new ReadWriteStream( this.stream, this.readPosition, this.writePosition, this.length );
        }

        public override long Length { get { return this.length; } }
        public override bool CanRead { get { return true; } }
        public override bool CanWrite { get { return true; } }
        public override bool CanSeek { get { return true; } }
        public override void Close() { }
        public override void Flush() { }


        [Obsolete( "Use ReadPosition of WritePosition", true )]
        public override long Position { get { return 0; } set { } }
        public long ReadPosition
        {
            get { return this.readPosition; }
            set { this.readPosition = value; }
        }
        public long WritePosition
        {
            get { return this.writePosition; }
            set { this.writePosition = value; }
        }

        [Obsolete( "Use ReadSeek of WriteSeek", true )]
        public override long Seek(long offset, SeekOrigin origin) { return 0; }

        public override int Read(byte[] buffer, int offset, int count)
        {
            long length = count;
            if ( this.length - this.readPosition < count )
                length = (long) this.length - this.readPosition;

            Array.Copy( this.stream, this.readPosition, buffer, offset, length );
            this.readPosition += length;
            return (int) length;
        }
        public override int ReadByte()
        {
            return this.stream[this.readPosition++];
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            CheckStreamSize( count );
            Array.Copy( buffer, offset, this.stream, this.writePosition, count );
            this.writePosition += count;
            this.length += count;
            OnDataAvailable();
        }

        public override void WriteByte(byte value)
        {
            CheckStreamSize( 1 );
            this.stream[this.writePosition++] = value;
            this.length++;
            OnDataAvailable();
        }

        protected void CheckStreamSize(int length)
        {
            if ( this.writePosition + length > this.length )
                SetLength( this.writePosition + length + ReadWriteStream.IncreaseSize );

        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        public override void SetLength(long newLength)
        {
            if ( newLength < this.length )
                throw new ArgumentOutOfRangeException( "newLength", newLength, "Truncating of data in the stream is not supported. The value of 'newLength' needs to be larger or equal to 'ReadWriteStream.Length'" );

            byte[] buffer = new byte[newLength];
            Array.Copy( this.stream, 0, buffer, 0, this.length );
            this.stream = buffer;
        }


        protected void OnDataAvailable()
        {
            if ( this.DataAvailable != null )
                this.DataAvailable();
        }

        public byte[] GetBytes()
        {
            byte[] buffer = new byte[this.length];
            Array.Copy( this.stream, 0, buffer, 0, this.length );
            return buffer;
        }
    }

    public enum EndianType
    {
        /// <summary>
        /// LittleEndian uses left to right reading/writing for numbers. This method is standard used on x86 hardware
        /// </summary>
        LittleEndian,

        /// <summary>
        /// BigEndian uses right-to-left notation for numbers. BigEndian is also known als Network Byte Order.
        /// </summary>
        BigEndian
    }

}
